Skip to content

feat: Story Mode, a Claude session drawn as comic pages (opt-in) - #813

Open
Ark0N wants to merge 109 commits into
masterfrom
feat/story-mode
Open

Ark0N wants to merge 109 commits into
masterfrom
feat/story-mode

Conversation

@Ark0N

@Ark0N Ark0N commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Story Mode: a Claude session drawn as comic pages, live

Watching an agent work means reading a terminal that scrolls past faster than anyone reads. That works for the person who wrote the prompt and knows the code, and shuts out everyone else: a manager, a designer, a new teammate, someone glancing at a phone. Story Mode adds a second way to watch a Claude Code session. Each turn (one prompt and everything Claude did for it) becomes one comic page: panels for reading code, making a plan, calling in helpers, editing files, running tests (POW! or KRAK!), asking you something and saving the work, each with a caption anyone can follow.

It is opt-in and off by default: App Settings → Header buttons → Story Mode (per device). On a phone it opens from Session Options (same setting). It reads the session's own Claude transcript through two new, experimental, read-only routes and draws in a full-window overlay; it adds no setting that changes how sessions run.

One change reaches beyond Story Mode: the server now also checks prompts sent by the browser's composers (Story Mode's prompt bar and the phone's Compose sheet) and presses Enter again while Claude Code is still ignoring it. See The Enter check below; it is the part of this PR that touches every prompt.

What you see

  • Three reading levels in the bar: Simple (plain words built from fixed templates, no paths, commands or jargon), Advanced (the story with file names) and Director's cut (every step and tool, with a list of every event of the turn beside the page). Director's cut is the default for now; a reader's pick is kept per device.
  • Everything is clickable. A file card opens Codeman's file viewer. The code page shows the diff of Claude's edits (Edit and Write calls exactly, with line numbers; a file written through the shell with its new lines; a file a script rewrote, listed without lines). A test board shows the output with clickable file:line links. A helper shows what Claude asked it, what it answered and every step it took, read from the helper's own transcript (live while a background helper works).
  • Live. A turn that is still running redraws as Claude works: new panels ink in where blue-pencil roughs stood, and the event list follows the turn with a smooth glide (scroll up and it stops, with a pill for new rows).
  • Fills the window. Panel rows are sized to the window (with a floor and per-row caps), text scales with them, and a panel whose caption or balloon needs room grows instead of covering its art or Claude's face.
  • A prompt bar under the page holds the turn picker and a field that sends Claude your next prompt (Enter sends, Shift+Enter adds a line), so you can keep a session going without leaving the comic. It sends through the same function as the phone's Compose sheet.
  • Save image downloads the page as a PNG to share with someone who never opens a terminal.

How it works

Server (src/story-mode.ts pure; src/story-reader.ts IO; src/web/routes/story-routes.ts):

  • GET /api/sessions/:id/story[?turn=<id>] draws one turn; GET /api/sessions/:id/story/helpers/:agentId draws one helper from its own transcript (<conversation>/subagents/agent-<agentId>.jsonl, the id checked against a strict charset). Both are documented in docs/api-reference.md as experimental.
  • Both read the session's OWN Claude transcript, never a path from the request: the path the Claude hook reported, only when it really lies (symlinks resolved) in a Claude projects folder (the session's relocated CLAUDE_CONFIG_DIR, or the server's), else a lookup by conversation id, then Codeman id. Story Mode is the first route that sends transcript content to the browser, which is why the hook path is confined. Only a regular file is read, through one non-blocking handle, so a FIFO in its place fails at once instead of pinning a libuv worker. Only the tail is read (8 MB, widened in steps to 64 MB while that tail holds no prompt to start the turn).
  • Three pure stages turn it into a page. Reduce each JSONL line to compact entries: this is where size is shed (Read results' contents, Edit/Write originalFile and images never leave this step; outputs keep a bounded tail; hunks are capped). Join each tool call with its result and split entries into turns (a background helper stays "running" until its <task-notification> arrives in a later turn, then its answer is joined back). Group a turn's events into at most 12 panels with captions for each level.
  • Grouping is what makes it read like a story rather than a log. Across every Claude transcript on the machine this was built on (1,309 files, 3,856 turns, 44,008 tool calls), Bash is 87% of all tool calls, so a Bash call is sorted by what its command does (looking, testing, building, editing, saving, running), not by its tool name: one-liners are split at &&, ||, ;, | and newlines outside quotes, here-document bodies are skipped, wrappers (timeout 60, env assignments, git -c) are stripped, and the strongest part wins. Calls in a row with the same intent share a panel; fix → test → fix → test folds into one "loop" panel; a long turn folds its quieter panels into a montage.
  • The route never adopts a conversation id or persists anything. It answers { supported: false, reason } for a CLI that writes no Claude transcript (registry capabilities.transcript, never a mode check) and for remote or Docker sessions, whose transcript is not on this host.
  • Responses carry a weak ETag; an unchanged transcript is a 304 decided from a stat, before anything is read.

Frontend (src/web/public/story-mode.js, story-mode.css): a full-screen overlay with a small canvas frame engine (each scene is a pure function of its frame number). Only new panels animate in; prefers-reduced-motion draws every panel finished.

  • Untrusted text: everything on the page comes from the agent and is written with textContent only, inside data-i18n-skip containers. On the server every helper that matches transcript text runs in linear time on crafted input (an agent can be prompt-injected, and one regex call blocks the whole server): timing each helper at its input cap found five shapes that were quadratic or worse (up to 29 s on 1 MB); they use indexOf scans now, and a test times them.
  • Files open through the existing openFilePreview(), which already handles workspace confinement, outside paths and remote hosts.
  • Keyboard: canvas click regions are real, labelled buttons; Tab stays inside the overlay; Escape closes one layer per press (a file preview opened from a panel, then the drawer, then the overlay).
  • Live refresh: the transcript tool events and the stop and prompt-submitted hooks (extra addListeners in connectSSE), plus a 4 s poll while a turn runs; one request in flight, debounced.
  • Fonts: two OFL comic fonts (Bowlby One SC, Patrick Hand SC; Latin subsets, about 23 KB each), self-hosted because the CSP is font-src 'self'; licenses in fonts/LICENSE-story-fonts.txt.

Performance, measured on this machine: a cold read of a 110 MB transcript takes 50 ms with a longest event-loop stall of 10 ms; a cached read about 0.1 ms; building a page 1 to 7 ms. A page carries at most 120,000 characters of outputs, hunks and helper texts (newest first); the largest live page in a sweep of every local transcript is 74 KB gzipped. Panels repaint by need (30 fps entering, 15 fps for a running turn's newest panel, 6 fps settled, stopped after 20 s): five panels on a 2x screen went from 81% of a core to 2.4% once settled. Stepping through 40 turns went from 46 long tasks to 1 (enough before to make the app drop the terminal's WebGL renderer).

The Enter check, also outside Story Mode

Claude Code 2.1.277+ ignores Enter for the first 30 to 50 s after its composer paints while still taking the typed text. The server already handles that for API prompts: SubmitVerifier (src/session-submit-verifier.ts) reads the pane on a 2 s to 60 s schedule and presses Enter again while the composer still holds the prompt. The browser's composers never send a text\r pair, though: they write one bracketed paste and a bare Enter about 120 ms later (Codex drops a key that shares a read with a paste), so that check never ran for them.

PastedPromptWatch pairs those two writes back up (in Session.write() for the WebSocket, and in writeViaMux() for the POST fallback) and arms the same check. Nothing may come between the two: any other write breaks the pair, and typed keystrokes plus Enter arm nothing. Changes to the check itself, which reach every API prompt too:

  • A prompt under 24 characters must now match the composer whole, so a one-word answer cannot read a dialog's highlighted row (❯ 1. Yes) as itself.
  • A multi-line prompt is recognised in both shapes Claude Code draws: inline (a short first line plus its continuation lines) and the [Pasted text #1 +3 lines] stand-in, which counts only when its line count is the prompt's own. Since master now pastes a multi-line API prompt with its line breaks, _verifySubmitted() keeps them for that count.
  • A paste made in the browser cancels any pending re-press, so Enter never submits the user's own paste.
  • Codex's [Pasted Content N chars] stand-in is still not matched (not measured).

How it was proven: the defect did not reproduce on Claude Code 2.1.303 (5 sends 0.3 to 1 s after the composer painted, all taken on the first Enter), so this is a safety net. A stand-in claude that draws Claude's composer and ignores Enter for its first 25 s reproduced it: sent from the real Story prompt bar in a browser about 5 s after start, the old build left the prompt stuck for 45 s on both the WebSocket and the POST fallback; the new build pressed Enter again at 2, 5, 10, 15 and 20 s and the prompt went through once at 25 s, the first moment it could. On real Claude Code the new build caused no extra Enter, and left alone for 65 s both text typed into Claude's composer after a send and a 4-line paste dropped into the pane.

Reviewing the diff

58 files, +20,294 / -27, but most of it is not code to read line by line:

Part Lines What to do with it
test/fixtures/story-corpus/ (11 recorded sessions + their pinned pages) 3,806 Skim one .expected.json to see what a page looks like
Tests (test/story-*.ts, test/routes/story-routes.test.ts, verifier tests) 5,419 Read alongside the code they cover
Font license text (the two .woff2 files are binary) 193 Nothing to review
Docs + changeset 62 Prose
New frontend: story-mode.js, story-mode.css 6,208 Self-contained overlay
New server: story-mode.ts, story-reader.ts, story-routes.ts, types/story.ts 4,361 The core worth review
Edits to existing files 245 / -23 in 14 files Read all of these

The edits to existing files are the ones that can affect anything else:

  • src/session-submit-verifier.ts (+91/-12) and src/session.ts (+36/-6): the Enter check.
  • src/utils/claude-transcript.ts (+64): finds a recovered session's (restored-<id>) transcript, the same recovery last-response makes.
  • app.js (+21): the SSE listeners for live refresh and a Story Mode entry in Shortcuts (no default chord).
  • index.html (+7/-2): the header button, the Session Options button, the settings chip, the script and stylesheet.
  • One-line registrations: the route and type barrels, server.ts, schemas.ts (showStoryButton, next to showResponseViewer), settings-ui.js (per-device key and visibility), i18n.js (the setting's zh-CN label), config/test-suites.ts (the browser test).
  • styles.css/mobile.css: the header button's hidden marker, kept off phones like the other new header controls.

The branch was built over several days and is 109 commits, including two merges of master (Merge master into feat/story-mode). The first resolved additive registrations plus session.ts, where master's public claudeConfigDir() replaced the branch's getter and the composer pairing moved into master's guarded _writeViaMuxNow; the second picked up master's frontend dead-code cleanup (one line in the src/web/CLAUDE.md z-index list). Squash-merging is fine.

Risk

  • The Enter check is the one change outside the feature: a wrong match would press Enter into someone's composer. The guards: it acts only while the LAST composer line holds the prompt (whole for short prompts), any other text or no composer line ends it, a newer write or paste cancels it, and the composer pairing accepts only a paste immediately followed by a bare Enter within 10 s. It was proven against a stand-in CLI because current Claude Code no longer shows the defect.
  • Transcript content goes to the browser for the first time. The read is confined to the session's own transcript inside a Claude projects folder, ownership-checked like every session route, and every string is rendered as text. The terminal card drawn on the canvas (which goes into a saved image) leaves out lines that look like secrets and masks addresses and long token-like strings, but that masking is a heuristic.
  • Wording heuristics (when a page says "Done, with a catch", "as you asked", "starts a git repository") were precision-checked by sweeping all local transcripts, but they are heuristics on another program's transcript format. test/story-corpus.test.ts pins every page of the 11 recorded sessions, so a change in Claude Code's format or in the wording shows up as a diff.

Reviewed against real sessions

Review agents started real Claude Code workers on an isolated Codeman instance, gave each a small program to build, watched Story Mode draw it live, then checked every panel against the transcript and the files on disk. Later rounds had cold readers (someone who has never written code, an engineering manager skimming, a new teammate, a fact-checker) read only the comic, write down what they believed happened, and only then compare with the recordings. Every finding is fixed with a test. Eleven of those transcripts, sanitized (paths, user names and emails rewritten, Read contents left out), are the regression corpus.

The twelve rounds and what each found
  • Round 1 (a Python CLI with pytest, a pomodoro page with node:test, a bash script with a background helper): a commit or test run past the first 400 characters of a long one-liner was classified from the display clip; a failing pytest run piped through tail drew as a pass; a background helper drew as finished with Claude Code's internal launch notes as its answer; logging into scratch files drew as code changes; balloons covered the art on narrow panels.
  • Round 2 (a prompt added mid-turn, background helpers then an interrupt, a question then failing tests first then a push): a prompt added mid-turn is a queued_command attachment with no user line; some sessions never write turn_duration; helpers can be teammates reporting in <teammate-message>s; files are often written through here-documents. Also commits vanishing into montages, a red test run tagged "ok", a failed push drawn as SAVED, and a full page reshuffling while live.
  • Round 3 (a written plan with a dry run, /compact then plan mode, keyboard-only use, image export, skins, a non-Claude session): a typed /compact drawn as two turns, a local command's turn left "working", the plan file under ~/.claude/plans/ counted as a code change, Tab walking out of the overlay.
  • Round 4 (a 110 MB real session; local commands and the advisor live): a scheduled run drawn inside the /compact turn before it, a turn with seven added prompts drawing past the 12-panel cap, and 46 long tasks when stepping through 40 turns.
  • Round 5 (a non-technical cold reader; a phone with touch): a push followed by a failing step drawn as a failed push, a formatter's preview read as a failure, a blocked command ending in confetti; Save image exporting blank panels below the fold, small tap targets, a drawer cutting through the bar.
  • Round 6 (keyboard, screen reader, contrast, reduced motion, 200% zoom): a commit then a push counted as two commits, an open drawer rebuilt on every refresh and not tied to its turn, focus dropped when Previous/Next disabled itself, text contrast down to 1.42:1 in the light skin.
  • Round 7 (security and fresh-eyes code review): quadratic cache eviction (a 2 s stall), a FIFO in place of a transcript pinning libuv workers, the hook path trusted for content, a hung request wedging the view across a reopen.
  • Rounds 8 to 12 (cold readers and fact-checkers): the closing panel kept only Claude's first sentences while what readers needed was further in ("the page will not work when double-clicked", a helper's tests that never ran). Claude's last words now keep one sentence of each kind (a problem left, a question or offer, how to use the result, something not done, a failed step), the final panel says "Done, with a catch/note/question", a failure you asked for reads "fails on purpose, as you asked", a save no prompt asked for gets a small line, a push to a folder on this computer says so, a stop names the step and the open to-do items. Each round's fact-checker verified every guessed claim (each new repository, each local copy, each "as you asked") against the recordings.

Known limits

  • Story Mode cannot tell whether Claude saved the work because a project rule asked or on its own; it says "a save your prompt here did not ask for" in a small line, never as a fault.
  • A stop names the step Claude was on and the open items of its to-do list; when the plan lives only in your prompt, what is left is not listed.
  • Files a command makes as test data count as changed files, and only the ones it names literally are listed.

Not in this PR

  • Other CLIs. Codex, OMP and DeepSeek have transcripts of their own; the page shape is CLI-neutral, but only the Claude reader exists.
  • zh-CN beyond the setting's label: the comic text is generated in English on the server, and translating only the chrome would leave a mixed page.
  • Accept/reject buttons on single changes (a new way to write files, so it needs a decision first).
  • Outputs on demand: a long live turn still sends its newest outputs with every changed page (at most 74 KB gzipped); a per-event route would make that a few KB at the cost of new API surface.

Testing

  • test/story-mode.test.ts (165 tests): Bash classification (segments, wrappers, probes, redirects into scratch, here-documents, scripts fed by one), test-count parsing (vitest, jest, pytest incl. all-failing and collection errors, mocha, cargo, node:test), the reducer (no file contents or originals survive, hunk counts, output tails, skipped meta/compact/sidechain/local-command entries, task notifications, interrupts, background commands), turns (queued prompts, partial turns, failed and pending calls, background helpers joined back), pages (captions at all three levels, balloons, loop marks, montage capping, the text budget, live panels), the wording rules behind every caption and title, and crafted inputs timed at sizes where the old code took seconds.
  • test/routes/story-routes.test.ts (25 tests): the newest page and an older turn by id; unsupported CLI, remote and Docker sessions; another user's session in multi-user mode; no transcript, a hook path outside the projects folder, a symlink out of it, a FIFO transcript and a FIFO meta file; a live turn growing between reads; a half-written last line, a replaced file, concurrent reads; 304 on an unchanged page; a turn larger than the first tail; the helper route.
  • test/story-mode-ui.test.ts (77 tests, JSDOM with a fake canvas): the real module draws every scene in every panel shape, opens every drawer, follows a live page, switches levels, sends from the prompt bar, keeps Escape and Tab order, and pins the fixes from the review rounds.
  • test/story-corpus.test.ts (39 tests): every page of the eleven recorded sessions, at every level; test/story-plain-words.ts checks that no Simple caption carries a path, command or jargon.
  • test/session-submit-verifier.test.ts (28 tests): the paste shapes Claude Code draws, the paste pairing, and a session-level suite for the WebSocket and POST fallback sends, a taken prompt, a newer paste cancelling, typing arming nothing, and a multi-line API prompt shown as the stand-in.
  • test/story-mode.browser.test.ts (6 tests, browser suite, not in the gate): a real server and Chromium; a running turn redraws live from real SSE events, the diff drawer, Escape order, Session Options → Story, Save image downloads a real PNG, no sideways scroll at 400 px, and panels that grow instead of covering or cutting off their words. Passes on the merged branch.
  • On the branch with master merged in: typecheck, lint, format:check, check:frontend-syntax, check:public-assets, check:browser-excludes, master's new check:frontend-dead and knip pass, and the full npm test gate: 17,440 passed, 13 skipped, 0 failed (on the first merge; CI runs it on the current head).

claude added 30 commits October 18, 2026 09:22
Story Mode is an opt-in header button (showStoryButton, per device, default
off, kept off phones) that opens the active Claude Code session as comic pages:
one page per prompt, panels for reading code, planning, helpers, edits, tests
(POW/KRAK), questions and saving, each with a plain story caption and a
Director's cut caption naming the tools. Files open in the existing file
preview, the code page opens the exact diff, a test board opens the output,
and a running turn redraws as Claude works.

Server: GET /api/sessions/:id/story (experimental) reads a bounded tail of the
session's own transcript. A pure core (src/story-mode.ts) reduces lines,
joins tool calls with their results, splits turns at prompts and
turn_duration, and groups events into at most 12 panels; Bash calls are
sorted by what the command does. The reader (src/story-reader.ts) caches
reduced entries and reads only appended bytes while the file grows.

Frontend: story-mode.js (canvas frame engine, live refresh from transcript and
hook SSE events plus a poll while a turn runs, details drawer, the wire) and
story-mode.css, with two self-hosted OFL comic fonts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codeman's file preview has no Escape of its own outside the folder grid, so with
Story Mode on top it stayed open and blocked the page. While Story Mode is open,
Escape now closes one layer per press: the preview, then the drawer, then the
overlay. A folder grid and a preview in edit mode keep their own keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…turn bigger than the tail

GET /story now sends a weak ETag (server start, transcript size and mtime, turn)
with Cache-Control: no-cache, and answers 304 before building anything when the
transcript has not changed, so a live view's polls cost a stat.

A turn can be bigger than the 8 MB tail on its own (tool results carry whole
files, ~60 KB per result line on large local transcripts), which cut its first
panels. When the window holds no prompt, the reader re-reads a 24 MB, then a
64 MB tail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n every shape

Runs the real story-mode.js against pages the real server core builds from
invented transcripts: panels, captions, balloons, the turn picker, the
Director's cut wire, file and diff clicks with their line numbers, output
links through the terminal's own path matcher, a live refresh that keeps
existing panels' clocks, activity filtering, Escape one layer per press,
teardown, error notes and the setting switch. A permissive fake 2D context
lets every scene draw every panel kind across its whole animation, at four
sizes, in both reading levels, without throwing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cores in balloons

Found by watching real workers: `node --test` was drawn as plain terminal work,
`git -c user.name=x commit` was not seen as saving, and a probe such as
`pytest --version` became a failing test. Commands are now split into their
simple commands (quote-aware, at && || ; | and newlines); each part is judged by
what it starts with after env assignments and wrappers like timeout and sudo,
git's own -c/-C options are skipped, a --version/--help probe is not a run, and
the strongest intent wins (test, save, build, edit, run, look). File
housekeeping (mkdir, touch, cp, mv, rm) and git init/add are plain commands.

Balloons dropped every _ and * as markdown (test_wcount.py read testwcount.py);
now only paired emphasis at word boundaries and line-start markers go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GET /api/sessions/:id/story/helpers/:agentId draws one helper (an Agent call)
as a page from its own transcript, <conversation>/subagents/agent-<id>.jsonl
beside the session's, with the type, description and finished flag from its
.meta.json. The file is derived from the session's own transcript path and an
id checked against [A-Za-z0-9_-]{1,64}; a helper with no file yet is page
null, not an error. The reducer gains a sidechain mode (every helper line is a
sidechain line) and keeps the agentId of Agent results, so the main page links
each helper to its steps. Same gates and ETag/304 as /story, now shared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A real server in test mode, real Chromium, a synthetic transcript handed over
through a real stop hook: off by default, the header button draws the running
turn, a grown transcript plus a hook redraws it live over SSE, the code page
opens the diff, Escape closes drawer then overlay, and a phone-width viewport
has no sideways scroll. Listed in BROWSER_TEST_GLOBS (nightly, not the gate).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ones, and polish

- Save image: the page as one PNG to share (each panel canvas, then every
  caption, balloon and chip redrawn from its computed style and box).
- The helper drawer lists what the helper did, read live from its own
  transcript (GET .../story/helpers/:agentId), led by the task Claude gave it.
- Session Options has a Story button (same setting), the way in on phones.
- A turn a background task or another session started reads as a notice,
  not as your speech balloon.
- Wide panels keep their scene in a centered stage instead of spreading the
  art to both ends; the fix panel's agent stands next to the page.
- An unchanged page (same ETag) is neither parsed nor redrawn.
- Paths inside the session's folder are shown short; clicks keep full paths.
- Tab stays inside the overlay; a missing 2D context skips the art, not the page.
- A rebindable Story Mode shortcut (no default chord).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every visible panel was repainted on every animation frame: five panels cost
~81% of one core (main thread, 2x screen, measured with Chrome's own task
counters). Panels now repaint at 30 fps while their entrance plays, 15 fps
for the newest panel of a running turn, and 6 fps once settled; a settled
panel holds still after 20 s until the pointer passes over it. Clocks still
run in real time. Measured: 11.6% in the first 10 s, 2.4% once the page has
settled, 5.1% with the pointer on a panel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ent loop

The first read decoded the whole tail as one string and parsed it in one
synchronous pass. Lines are now decoded one at a time straight from the buffer
and the loop yields every 1 MB. Measured on the largest local transcripts
(110, 64, 49 MB): a cold read takes 39-50 ms instead of 100-123 ms, and the
longest event-loop stall is 8-10 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three review agents watched real Claude Code workers build small programs
and compared every panel with the transcript. Replaying their transcripts
through the core found, and now fixes:

- A Bash call was classified from its 400-character display clip, so a
  commit or a test run late in a long one-liner vanished (a failing pytest
  run drew as "saves the work"). The intent, the printed files and whether
  a save ships are now read from the whole command.
- A test run that reports failures failed, even when `| tail` hid the exit
  code. With that, the real fix-test-fix-test sequence folds into one loop:
  "Fix and test, 2 rounds. It passes in the end."
- node:test (`# pass 7`), all-failing pytest (`3 failed in 0.1s`) and
  pytest errors are counted.
- A helper started in the background was drawn as finished, with Claude
  Code's internal launch notes as its answer. It now stays running until its
  task notification arrives; the answer, tool count and duration are joined
  back onto it, and the final panel says when helpers are still out.
- The notification turn reported Claude Code's own duration, counted from
  the first prompt (2 min 38 s for a 21 s turn); a turn is capped at its span.
- Logging into scratch (`| tee "$S/dry.txt"`, `> /tmp/x`) counted as an edit;
  redirect targets are read quote-aware, and a here-document's body is data.
- A probe (`pytest --version`) reads as looking around, not running a tool.
- A plan Claude writes as a numbered list (no plan tool, as in current
  Claude Code builds) is drawn as a plan panel; files it cats are file cards.
- A balloon: code spans stay whole, an ellipsis or e.g. does not end the
  sentence, a remark about the check that just ran stays on that check, and
  a loop shows the last word of its rounds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s that keep up

From review round 1, checked against screenshots of the reviewers' real
transcripts at desktop and phone width:

- Claude's balloon is placed in the band above the head the scene drew, its
  tail pointing down at it, and clamped to stay inside the panel and off the
  head. Per-kind CSS offsets had covered the POW burst, the agent's head on a
  half-width fix panel and the first line of a terminal; a long prompt ran
  out of the mission panel. Save image clamps the same way.
- A board only shows real counts; a check that reported none shows one
  verdict instead of 21 invented dots, and the loop shows its last run.
- The mission panel opens the whole prompt, and on a task turn what the
  background job reported.
- A background helper says "in the background" and "still working" until it
  reports back, its drawer keeps polling while it works, and the final panel
  counts helpers still out.
- A fix made from the terminal opens what ran instead of an empty diff.
- The wire files Claude's words under the panel that draws them, and its
  last words under the final panel.
- A drawer follows its panel by the first event it showed when a refresh
  regroups panels (a check folding into a loop), not by kind.
- Click regions reused for a new page get the new label; a new turn draws
  all its panels fresh.
- Output links skip globs and paths a tool elided; prose links rooted paths
  only; paths inside the session folder read short (clicks keep the full
  path), and files Claude printed with cat open relative to that folder.
- File cards keep a name's separators (rename_by_ / date.sh), code drops
  ligatures (`!==` stays three characters), counts drop "−0", and the turn
  picker's text stops before its chevron.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and end turns

Found by replaying round-2 review transcripts while the reviews ran, then
surveyed across 150 local transcripts:

- A prompt typed while Claude works writes no user line in current builds,
  only a queued_command attachment where Claude took it in (602 of them
  locally, 485 of those background-task reports). It now joins the running
  turn; a report that lands mid-turn is a notice on the wire, not a new
  mission panel, and a background helper's answer joins back from it.
- Some sessions never write turn_duration (13 of 135 locally): the Stop
  hook's stop_hook_summary (never preventedContinuation in 815 seen) now
  ends the turn, timed by its own span. Before, such a session's turns
  never ended.
- A helper started as a teammate (teammate_spawned) is a background helper;
  its <teammate-message> reports (text or a JSON idle notification) join it
  by name, and the turn they start reads "readme-writer: README done".
- A <cross-session-message> names its sender; pasted text loses Claude
  Code's <pasted_content> wrapper (it showed in balloons and the picker).
- The mission panel, its balloon and its drawer follow who sent each
  prompt (you, a background job, another session), not who started the turn.
- A caption names a script by its first line that says something, not
  `set -e`, a bare `cd` or `S=/tmp/...`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd at a check

From review round 2 (a question, failing tests first, and a push):

- A run that reported failed tests is now failed on the event itself, so
  its drawer and wire tags agree with the panel (the TDD red run was a red
  panel with a green "ok" tag; `; echo "exit=$?"` had hidden the exit code).
- A save whose command failed says so ("Claude tries to send the work up,
  and the command fails."), stamps ERROR instead of SAVED, and counts the
  error in the Director's cut. A save still running stamps nothing.
- A loop ends at its last check: README, .gitignore and the like written
  after the tests passed are their own fix panel instead of disappearing
  into the loop's diff.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shortcut registry gained a Story Mode entry; every registry label must
translate (test/i18n-exit-run-help.test.ts, caught by the full gate).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, from review round 2

- Files a Bash call writes through here-documents (`cat > f <<'EOF'`, `tee`)
  are whole-file edits on the event: in the diff drawer, on the code page,
  in "N files changed". Before, a worker that wrote seven files that way
  drew a page with zero files changed. Labels leave the bodies out (the wire
  row was a wall of file text), and a relative target resolves against the
  transcript line's cwd unless the command changed directory first.
- A caption names the part of a one-liner that decided it (`node --test`,
  the commit), not its first line.
- The advisor (a server tool, answer encrypted in the transcript) is drawn:
  "Claude asks the advisor for a second opinion." It is not counted as a
  helper. Server web search maps to the web panel.
- Checks that all pass from the start read "Build and test, N rounds. Every
  check passes.", not as fixing.
- pytest's collection errors are counted apart: "The tests don't even load."
- A result remark replaces a step announcement on its check.
- A turn still open when the next prompt starts has ended; an interrupted
  turn is timed by its span.
- The mission drawer keeps prompts up to 4000 characters.
- Drawer: diff headers, titles and commands read relative to the session
  folder (the folder itself reads "."); a bare name under an `ls dir:`
  header links into that folder; the chosen answer is marked, not struck
  through; boards count errors.
- While a question waits, the pencils say "Waiting for your answer…";
  balloons start below the LIVE tag; the loop's balloon points at its
  verdict, above the board.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…holds still

From review round 2 (a long turn, and a prompt added mid-turn):

- Commits fold last and a montage names them ("saves the work in 3
  commits"); a save counts its commits ("saves the work in 6 commits").
  Before, all three commits of a 16-step turn vanished into montages that
  read "runs 2 commands".
- Folding a full page starts at the oldest pair and grows a montage already
  there, and never takes the newest panel while another choice exists: a
  live page no longer reshuffles panels the reader is watching.
- A check whose output reports problems ("1 file would be reformatted",
  "Found 3 errors", "✖ 5 problems", `error TS…`) failed, though a trailing
  `; ls` hid the exit code: "The tests pass, but another check fails."
- Shell structure is not a command (`for x in`, `do`, `done`, `if`), so a
  `--version` probe in a loop reads as looking; `command -v` and
  `git config --get` too.
- The montage draws only the kinds of steps it folded, with a box for
  saves; the loop's arrow has a head and rests on a finished page; the
  "MISSION COMPLETE!" title has room for its tilt; drawer commands keep
  their line breaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Commits are in." landed on the check panel after the save it reports on;
the rule that keeps a result remark on its check now covers saves too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A local slash command (`/compact`, `/model`) gets no reply from Claude and no
turn_duration, so its turn stayed "Claude is working" until the next prompt.
It now ends at the command's own output (<local-command-stdout>, kept for
the mission drawer) and reads "You run /compact.". `/compact` writes its
compaction marker before the command line itself; a marker that lands
between turns now belongs to the turn after it (it was drawn at the end of
the previous, finished turn), and one mid-turn stays where it is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…planning

From review round 3 (compaction, then plan mode):

- A typed `/compact` and its <command-name> record share one promptId; they
  were two turns ("Done in 0 seconds", then a turn left running). A prompt
  carrying the open turn's promptId now joins it: one "You run /compact."
  turn of 24 s, the real compaction time. A command typed as such
  (`/model opus`) is a command too.
- Plan mode: writing the plan file under ~/.claude/plans/ is planning (a plan
  panel with its numbered steps), not a code change ("3 files changed" for
  a commit of 2). ExitPlanMode's input is empty in current builds; its
  result carries the plan and your answer: "Claude shows you its plan, and
  you approve it."
- A command's Director's caption names it ("Command · /compact").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 3 found missing

From review round 3:

- Keyboard: a file opened from a panel or drawer takes focus on the
  preview's close button, Tab cycles inside the preview while it is open,
  and Escape gives focus back to what opened it. Before, Tab walked out of
  the overlay to the session tabs behind it. A panel rebuilt under focus (a
  new turn, a fold into a loop) hands focus to its replacement.
- A commit at the end of a test one-liner is no longer lost: commits and
  pushes are counted whatever intent wins, and the check says "The tests
  pass, and Claude saves the work." (plus "1 commit" in the Director's cut).
- Files written by `printf/echo ... > f`, and files Claude Code says a
  command changed ("This command modified 1 file you've previously read"),
  count as changed files.
- A compaction says what it did ("automatic · 46.9k → 3.6k tokens · 24 s");
  an automatic one reads "Claude's memory is full", and its panel opens.
- ToolSearch is looking around, not a terminal panel; EnterPlanMode reads
  "Claude switches to plan mode"; the plan drawer shows the plan you
  approved ("You approved the plan.").
- A finished newest page is checked every 15 s (a 304 when unchanged): a
  local command like /compact raises no hook.
- Boards draw the real number of dots (5 tests, 5 dots); captions leave
  here-document markers out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A report's turn links to the helper it answers: its drawer shows what the
  helper did. Reports arrive HTML-escaped (`&lt;a@b.c&gt;`) and now read as
  text.
- Screen readers: a polite status region says when a new turn starts and
  when one finishes; the note is a status, the drawer a dialog named by its
  title; "Open file" buttons name their file.
- Save image draws the balloons' tails (from their CSS triangles) and is
  disabled with nothing to save.
- App toasts move to the bottom while Story Mode is open: at the top they
  covered its bar and turn picker on phones.
- The bar names the session as its tab does; a click on the mission balloon
  opens the prompt.
- A heading ends at its line (a plan's "# Plan: add wrap" no longer runs
  into its next paragraph) and a short one ("## Summary") is dropped; the
  plan you approved stamps "You approved it".
- "Claude's part is done in 3 minutes. A helper is still working."; the
  helper steps say "still working" while the helper runs; the advisor's
  labels call it a second opinion; a check run twice the same way reads once
  with a count; the session folder reads "." in commands only, never in
  prose; the loop's POW keeps off its board on a narrow panel; a shell
  session's note says it runs a shell or another CLI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A turn with no end marker of its own (an abandoned or crashed turn) was
closed when the next prompt started and then drawn as "Mission complete!".
It now reads "To be continued…" / "The trail stops here: the next prompt
starts a new turn.", and the page carries `superseded`. The Director's cut
names what ended every turn: turn_duration, stop_hook_summary, a local
command, an interrupt, or the next prompt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nine transcripts from the review rounds, each a real Claude Code worker
building a small program (a Python CLI with a fix loop, a web page with
node:test, a bash script with a background helper, a long turn with a
prompt added mid-turn, teammates then an interrupt, a question then tests
first then a push, a text plan with a scratch dry run, /compact then plan
mode, a test failed on purpose). Each is cut down to the lines and fields
the reducer reads, with every path, user name and email rewritten and the
file contents of Reads left out; replaying them before and after gives
the same pages.

The expected file beside each pins every page (panel kinds, captions,
balloons, counts), so a change that alters a story fails with a diff of
exactly what changed; STORY_CORPUS_UPDATE=1 regenerates them. The beats
the reviews were about are also stated outright.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eleven batches of findings had grown it by accretion. It now reads in
parts: the routes, where turns start and end, how background work joins
back, and what a page, an event and a panel carry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ull of added prompts

From review round 4 (a 110 MB real session, about 23 turns, read through
all four tail steps):

- A scheduled task's run was drawn inside the /compact turn before it (96
  minutes and 237 tool calls credited to "You run /compact."): its prompt is
  a meta line, and work after an ended turn was appended to that turn. The
  scheduled prompt now starts a turn of origin `scheduled` ("A scheduled
  task starts this turn."), and work that comes more than 5 s after a turn
  ended starts a turn of its own ("Claude carries on"); a line Claude Code
  flushed a moment late still joins.
- A turn with seven prompts added mid-turn drew 14 panels (the cap is 12):
  missions were never folded. An added prompt may now fold into a montage,
  as the last resort after quiet panels and work panels, and the montage
  says so ("Claude keeps going (you add 2 things): ..."); the turn's own
  prompt and a question to you are never folded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t of review round 4

Stepping through 40 turns made 46 long tasks of 51 to 286 ms, enough for
the app's guard to drop the terminal's WebGL renderer for the rest of the
page's life. Drawing switched off made none: the cost was canvas raster at
paint time, which the per-frame JS budget never saw. Now:

- Panel backgrounds are shared between panels and turns (kind × size ×
  palette), and a new one is built in four bands, one per frame (a wide
  halftone at 2x was ~35 ms in one go; a band is ~8 ms).
- At most two panels are drawn per frame, the first one served rotating so
  none starves; clocks still run for all.
- Measured on 20 turns stepped 40 times at 2x: 1 long task (52 ms), against
  39; the settled overlay is still ~2% of a core.

Also from round 4:
- The turn picker says when older turns were not loaded (`older` in the
  response: the transcript is longer than the part that was read).
- A compaction after the last turn closes that turn; a local command's
  output is the final panel's balloon ("Compacted"), without its markdown
  ticks, and a turn under a second reads "Done.".
- Exact durations keep their seconds past an hour, so the two captions never
  disagree on the minute ("1 h 10 min." / "1 h 9 min 39 s").
- A turn that arrives already finished is announced as new too.
- Whole files written by here-documents read "Claude writes 3 files."; the
  advisor's caption reads "a second opinion" instead of repeating its name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cold-reader review

Review round 5 had an agent read only the comic of the nine real sessions
(20 turns), write down what it believed happened, and only then compare
with the transcripts. Three panels told it something false, one hid the
failure the user asked to see:

- A push that went through, followed by a check that failed in the same
  command, drew as a failed push; the output (`[new branch]`, `->`) now
  decides: "Claude saves the work in 2 commits and uploads it, but a step
  after that fails."
- A formatter's "would be reformatted" preview, printed before it fixed
  the file, made a clean run a failure: the last verdict wins.
- A blocked command ended in confetti and "Mission complete!": a run panel
  where everything failed says "Claude tries a command, and it is
  blocked.", and a turn where nothing worked ends "Turn over" / "Nothing
  Claude tried worked, so the job is not done." with a neutral robot.
- The loop stamped only the final verdict (POW ×2), so a first run that
  failed on purpose never showed. It now marks every test run red or green,
  and says it: "Fix and test: the first run fails, the second passes." /
  "Build and test, 4 rounds. A check fails once, then everything passes."

Also:
- Files written inside a check or a save reach the plain caption ("Claude
  writes 4 files; the tests pass.").
- Balloons keep the next sentence when the first reports a problem ("…left a
  stray file. I'll remove it…") or is very short ("Not done. The harness
  blocked sleep 65…"); Claude's last words keep a caveat that follows them
  ("Nothing is committed because this folder isn't a git repository.").
- The advisor never shares a panel with helpers that do work; ToolSearch
  reads "Claude gets the tools it needs ready."; Codeman's own case file is
  not drawn as a file Claude read; looking around shows what Claude meant
  (the call's description), not raw commands; a montage at the start of a
  turn reads "Claude gets started".
- Words: boards count tests ("19 tests"), "uploads it" replaces "sends it
  up", and the prompt balloon keeps 400 characters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review round 5 (a phone with touch):

- Save image on a phone exported blank art for every panel not yet scrolled
  into view (the frame loop draws only what is on screen). It now draws
  every panel completely first, and the file name carries the turn number,
  so saves of different turns no longer overwrite each other.
- A test command sent to the background ("Command running in background
  with ID …") drew as "The tests pass." with POW before any test ran. It is
  now running: "Claude starts the tests in the background.", tagged "in the
  background" on the wire, and the final panel says a command is still
  running. Its task notification settles it from the exit code, and reading
  the job's output file that reports tests is a check ("The tests pass.",
  7 passed) instead of "Claude looks around".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… what they saved

From review round 5 (a phone with touch):

- Touch screens: every control at least 44 px (prev/next, close, the
  Story/Director's cut toggle, Save image, the final panel's chips, drawer
  buttons and links), and the small type at 13 px.
- The drawer on a phone starts below the bar (it cut through it), closes by
  dragging its head down, and keeps its head small in landscape.
- The turn picker says where you are ("Turn 2 of 5"), learns about new
  turns while you look at an older one, and a turn you picked yourself is
  not announced as news.
- A check whose command also wrote files offers "See the 2 files it wrote".
- Test counts are read from the whole output: a later command's lines had
  pushed pytest's "5 passed" out of the kept tail.
- A save names its commit: "Claude saves the work: "feat: add wrap() for
  modular wrapping into a range"." (from -m, -am, --message or the
  here-document of git commit -F -; a trailer is never the subject).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
claude added 30 commits October 18, 2026 17:56
…ering the art

From review round 11: a long caption ("Claude writes pyproject.toml
and changes test_csvstat.py; the tests pass, then Claude starts a git
repository and saves the work on a new branch …") covered the test
board, the POW and the code card's file name on a two-column panel.

A caption that reaches past the band above the art now makes its panel
taller by the difference (re-fitted on resize and on the Director's cut
toggle). Every scene keeps the panel's usual height as its stage and
stands at the bottom, the background filling the space above, the way
the final panel already did; Claude's balloon is placed against the
moved head. The browser test forces a long caption and checks the panel
grew and the caption ends above the art.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hat it saved"

From both round-11 readers: `git --git-dir=origin.git log -1 master;
git status -sb | head -1` after a push read "Claude looks around the
project", so a reader could not tell the save was verified. A look made
only of git lookups (log, status, show, ls-remote, …, piped to head or
tail) after a save in the same turn now reads "Claude checks what it
saved."

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r loaded says so (review round 11)

From the fact-checker's last items:
- A helper's report quoted in a notice joined its first sentence to one
  from paragraphs later with nothing between: where text was left out,
  the balloon now says "…" (found in the report itself, so two
  sentences that follow each other are never marked).
- "The first run fails on purpose, as you asked" for a run that never
  loaded a test (a collection error) now reads "At first the tests don't
  even load, as you asked; the second run passes.", as a test panel
  already says it.
- A list intro without a count ("What failed and how I fixed it:") brings
  its items while they fit in 360 characters, not 260.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry runs (review round 12)

From a reader who has never written code:
- "a save your prompt here did not ask for" on a save that was step 3 of
  the plan you approved ("Verify and commit"): a commit step in an
  approved plan asks for the save. When Claude says a project rule asks
  for its commits (read on its whole message), the line says "…; your
  project's rules do".
- "a check after that fails" read as a failed test where it was `git
  describe` finding no tags: "the version-tag lookup after that fails
  (there are no tags yet)".
- The dry run you asked for never showed: the terminal card took the
  last command's edge-case errors. A dry run in the panel is now what the
  card shows, its "a -> b" lines, under "Claude does the dry run you
  asked for: it shows what would happen, changing nothing."

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…is checking what came back

From review round 12: after a helper reported back, Claude's `git log`,
`git show --stat` and a diff of the README example against real output
read "Claude looks around the project". In a turn a report started, a
look that names no file now reads "Claude checks the work that came
back."

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… says so

Flagged by four readers across rounds 10 to 12: after a push, "but a
check after that fails" never said that Claude repaired what the check
found (the new remote pointed at a branch with no commits) and checked
again. When Claude's next command runs that same check again after a
change (`git -C remote.git symbolic-ref HEAD refs/heads/main && git -C
remote.git log …`) and passes, the caption adds "; Claude fixes it, and
the check passes next." A check re-run with no change before it, or one
that fails again, claims nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Story Mode

The owner wants to keep working from the comic view: "on the bottom have an
input field! so I can send more input in that view!". Story Mode gets a bar
across the bottom (the overlay's new last grid row): a field that grows from
one line to about five, a Send button and a one-line hint.

- It sends to the session the overlay shows (st.sessionId), never the active
  tab, since Session Options opens Story for any session. Delivery reuses the
  mobile composer's tested path, KeyboardAccessoryBar._sendComposedPrompt: one
  bracketed paste on the durable exactly-once queue, then a separate delayed
  Enter. Text plus a carriage return in one burst would be read as a paste by
  Claude Code and strand the prompt.
- Enter sends, Shift+Enter adds a line, an IME composition and whitespace-only
  text never send.
- While Claude waits on a question or a permission in the terminal (a pending
  question on the newest page, or the permission/elicitation hook), sending is
  off: the message would land in the selection dialog, whose Enter picks the
  highlighted option. The field still takes a draft and the hint says why.
  A session that is gone (not in app.sessions, or a 404 from the story
  route), whose agent exited, or that Story cannot draw (a shell would run the
  text) keeps its draft readable but sends nothing. A running turn may be
  sent to: Claude Code queues the message.
- Drafts are kept per session in memory for the life of the page.
- After a send the field clears and keeps focus; a reader parked on an older
  turn (or held there by a drawer) goes to the newest, since activity is
  ignored while parked, and a refresh is scheduled.
- Escape in a field holding text steps out of it first (focus to Close), an
  empty field closes one layer as before, and an Escape that cancels an IME
  composition closes nothing.
- The bottom safe-area inset moves from the overlay onto the composer, so its
  background reaches the edge; the drawer ends above the composer and toasts
  sit above it (measured into --story-compose-h). Not part of Save image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Story Mode gets a third reading level for someone who has never seen a
terminal (a parent, a manager). Every panel now carries caption.simple:
one short sentence in everyday words, with no file name, command, tool
name, flag or jargon. The sentences are templates only, so nothing from
the transcript (a path, a commit message, a branch) can reach them, and
they are written from the same facts as the story caption: a failed
check never reads as a pass, a failure you asked for says so, and a
check that fails after one passed is a failure again.

The final panel's simple caption is a plain summary: how the turn ended
and roughly how long it took, how many files changed, whether the checks
pass, whether the work is saved (only claimed "not saved" where saving
is in play: you asked for it, or the turn used git), and what the reader
must not miss (a problem left, a note, a question for them).

test/story-plain-words.ts is the guard: the corpus test runs it over
every panel of every page of the eleven sessions, and the unit tests
cover the honest endings (failed check, catch, note, question, stopped,
not done, to be continued, done for now) and the guard itself. The
corpus expectations only gain "simple" lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s cut

The bar's two-button group becomes three, in reading order: Simple (new,
for a reader who has never seen a terminal), Advanced (the story view as
it was) and Director's cut (the wire and the tool captions, unchanged).
The Director's cut is the default, as the owner asked; the reader's own
pick is kept per device under codeman:story-level, and only a click
stores it, so opening Story Mode never writes the default back and
changing the default stays a one-line change. _storySetDirector(on)
stays as a shim (on: Director's cut, off: Advanced) beside the new
_storySetLevel(level); st.director stays a boolean for the wire and the
scenes, which see director false at Simple.

At Simple each panel shows caption.simple in the letter font, a size up
(calc(20px * var(--story-k, 1))). Claude's balloons are hidden (its
narration is often technical; the art carries the panel) while your own
prompt balloon stays. The final panel shows the plain summary instead of
Claude's last message, and its chips are the time, the files changed and
"Read Claude's message", which opens a new drawer view with Claude's
last words as it wrote them. Balloons are hidden with the hidden
attribute, so Save image (which skips hidden boxes) prints what the
screen shows. On a phone the three buttons and Save image share one row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the Director's cut as the default reading level, the story line the
test lengthens is not shown, so its panel never grew and the check
failed. The test now picks the Advanced level first, where that line is
the caption.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…th bar row

With three reading levels the third bar row (the levels and Save image)
is full at 390px, and a running turn's LIVE pill wrapped onto a fourth
row (the bar went from 170 to 222px). On a phone LIVE now sits on the
title row, where the session name gives way to it; the bar stays at
three rows either way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Director's cut rebuilt the whole wire with replaceChildren on every
refresh (every 4 s poll and SSE burst of a running turn), so it never
followed new events and a rebuild could throw the reader's place, focus
and hover away.

Rows are now keyed by event id and headings by the row they head (a panel
that changes kind or number as the turn grows keeps its heading node).
Unchanged nodes are reused, a changed row is patched in place (a running
tag flips to its result on the same node), new rows are inserted in
order with a short transform and opacity entrance, and a new turn
replaces the list. _storyEventRow still builds every row.

While the wire follows, it glides to the newest row with a critically
damped spring on animation frames: a burst of updates moves the target
and the glide keeps its speed, so there is no stop and restart. Measured
against the browser's own smooth scrollTo on a growing transcript, the
spring moved a median 8 px a frame over ~25 frames with no stalls, while
scrollTo snapped at ~41 px a frame over ~10 frames and its read of the
new scrollHeight forced a layout inside the update (a 57 ms long task).

Scrolling up by hand (wheel, touch, scroll keys, the scrollbar) stops
following and shows a pill that counts the rows that came in since;
clicking it, or scrolling back to the bottom, follows again. The glide
only moves down and is never read as the reader scrolling up. A running
newest turn, and the Director's cut turned on during one, open at the
bottom, following; a finished turn opens at the top. Under
prefers-reduced-motion there is no entrance and the list jumps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A balloon that found no room beside a long caption dropped to just below
it, inside the art band: on the fix loop it covered the code page and the
"19 tests" label of the board, and elsewhere it sat on Claude's face (a
balloon of three lines or fewer was exempt from the clamp). A sweep over
the 11 corpus sessions, every turn, both modes, at 390, 820, 1100 and
1440 px and beside the diff drawer found 131 of 690 balloons covering a
click region or Claude's face.

Every balloon (Claude's, the loop's aimed one, the ask panel's, yours and
a notice) is now placed by _storyFitBalloon from the scene's layout at
its finished frame (StoryArt.layout, a dry run on a scratch canvas), so
an entrance with objects still in flight never moves it or the panel's
height. It must end above every click region under it (its tail above
those under the tail) and above Claude's face; a click region holding
his face is Claude himself and only the face rule applies to it. When
two lines do not fit, the panel grows for caption and balloon together
and the scene stands lower. The growth is computed at the panel's usual
height (storyBalloonRoom, pure), so a refit of the grown panel asks for
the same height: one extra pass, never an oscillation. The rest of a
long text clamps as before.

agent() now names Claude by his default colour, not by being cyan, so
the cyan fourth helper no longer takes the balloon, and the check scene
reports how high Claude's jump lifts his face.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…is held over

On a narrow panel (a phone, a two-column page) Claude holds the lens over
the investigate terminal, and its rim covered the end of the second line
and the ones below it. Those lines now end with an ellipsis short of the
lens's reach (over every frame of its sway); the lines above and below
it keep the terminal's full width, and on a wide panel, where the lens
hangs beside the terminal, nothing changes. A narrower terminal was the
other option and cut every line to about 14 characters on a phone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Story Mode entry still said _storyPlaceBalloon places Claude's
balloon over the head agent() last drew. Placement moved to
_storyFitBalloon, from the scene's finished frame, with the panel growing
for caption and balloon together; the rule now says so, and names the
two things a change must keep: never place from the frame on screen, and
compute the growth at the panel's usual height so a refit cannot
oscillate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The comic sheet was capped at 1240px with empty margins beside it, the
Director's cut wire was a 320px box, and a short page ended half way down
the window. Now the stage takes the full width (16px gutters kept), the
wire is clamp(320px, 25vw, 460px) wide and runs the main area's whole
visible height, sticky, its list scrolling inside, and the rows grow to
fill the visible height.

Row heights come from a pure function next to storyLayoutSpans
(storyRowHeights): the visible height is shared evenly between the page's
rows for the active column count (6 over a 1000px sheet, 2 over 600px; one
column keeps its natural heights), never under 270px (a longer page keeps
270px rows and scrolls as before) and never over a cap: 540px (twice the
drawn size: past it a one-row page is mostly background and the text, which
stops growing at 1.45x, looks lost), and the height at which a row's
narrowest panel would get a scene stage under 280 units wide (the mission's
exclamation mark ran off the panel below that, which is where a span-2 panel
turns into a portrait). What a capped row cannot take goes to the rows that
can, mostly the final panel's full-width row; its scene keeps a stage wide
enough (440 units) to stand beside its words. When the final panel's
in-flow words make its row taller, the other rows give that height up, so
the page still ends at the bottom. A capped page leaves blank paper, never a
band of background: the sheet reaches the visible bottom.

The values are plain px written from JS (each panel's --story-panel-min,
the page's grid-template-rows and --story-row-h, which also feeds
grid-auto-rows, so there is one 270 default), and --story-panel-min is
registered as a <length>: _storySizePanel and _storyFitCaption read it with
parseFloat, which a raw calc() would have turned into NaN. The comic text
scales with --story-k (the shortest row over 270, at most 1.45), set on the
page; the you/notice balloons sit below the caption by the same factor.

It is recomputed by the existing ResizeObserver, which now also watches the
main area (window, drawer padding, a bar added later) and the sheet's width
(the Director's cut toggle), once per delivery, and on every render. It
reads boxes that its writes cannot change, and the main area keeps its
scrollbar gutter, so it never oscillates; panels still resize through
_storySizePanel, whose draws the frame loop rations. Main's bottom padding
is 16px.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…kground cache by bytes

The rows were re-fitted when the main area or the sheet's width changed, but
a change of words alone (the Director's cut toggled with the wire stacked
under the sheet, another reading level, a caption that grew, the fonts
arriving) leaves both alone while the final panel's in-flow words take
another height. The resize observer now watches the page's height too and
re-fits on the next frame: the same boxes give the same values and nothing
is written, so it settles (measured: 0 passes while idle, at most 2 per
change).

Bigger rows mean bigger panel backgrounds: 32 of them at 2560x1440 and a
2x screen came to 342 MB. The cache keeps at most 160 MB as well; a panel
that shows a background it lost keeps its own copy, only reuse is lost.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/web/public/story-mode.js
…nced

The Director's cut is now the default level, so a test that turns it on right after opening
changed nothing and the wire was never placed. Start from Advanced, as a reader would.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The owner asked to merge the turn picker with the new input field. The prompts you already sent
(the picker and its arrows) now sit on the left of the bottom bar, the next message beside them,
the hint under the field; on a narrow screen the turns get their own row above it. The top bar
keeps the title, the reading levels, Save image and close, so a phone's bar is two rows. On a
touch screen the ready hint (Enter and Shift+Enter) is hidden: it means nothing on a touch
keyboard, and the hints that matter still show.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…line

A panel ignores a growth of 2 px or less, but the clamp measured against
the exact room, so a balloon that fell 0.6 px short of two lines showed
one. The clamp now lets a balloon use those 2 px of the 4 px gap it keeps
from the art and Claude's head, which is what the ignored growth would
have bought, and rounds away float error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/web/public/story-mode.js
… picker is gone from it

The picker took a row of its own and so pushed the reading levels to the next line. With it in the
composer, close squeezed in beside the title and Save image wrapped alone. A full-width
pseudo-element between close and the levels breaks the row; its height is the gap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ing levels, wire and composer

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…her prompt

Story Mode's prompt bar and the phone's Compose sheet send a prompt as two
writes: one bracketed paste, then a bare Enter about 120 ms later. Neither is
a `text\r` pair, so SubmitVerifier was never armed for them, and a send while
Claude Code was still ignoring Enter (2.1.277+, the first 30 to 50 s after the
composer paints) sat unsent.

PastedPromptWatch pairs the two writes back up, in Session.write() (the
WebSocket path) and writeViaMux() (the POST fallback's Enter), and arms the
verifier with the pasted text. Nothing may come between the two, and a newer
paste cancels a pending check, so Enter never sends the user's own paste.

The composer match learns what a pasted prompt looks like (measured on
2.1.303): continuation lines count for a short first line, and Claude's
`[Pasted text #1 +3 lines]` stand-in counts only when its line count is the
prompt's own. A prompt under 24 characters must now match whole, so a short
answer cannot read a dialog's highlighted `1. Yes` row as itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts were additive registrations (types barrel, routes barrel, server,
index.html, settings keys, docs) plus three in session.ts:
- master's public claudeConfigDir() method replaces the branch's getter;
  Story Mode's route now calls it.
- writeViaMux keeps master's guarded-turn structure; the composer pairing
  (_notePastedPrompt) runs after _verifySubmitted in _writeViaMuxNow.
- the imports keep both sides.

Master now pastes a multi-line API prompt with its line breaks, so
_verifySubmitted keeps them too (the same text TmuxManager.sendInput pastes):
a prompt Claude Code shows as "[Pasted text #1 +3 lines]" is recognised by
its line count, and the CLAUDE.md gotcha no longer says it is missed.
The changeset and Settings Reference describe the three reading levels and
the prompt bar.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Picks up master's frontend dead-code cleanup. The one conflict was the
z-index list in src/web/CLAUDE.md: master dropped removed layers, the branch
added the Story Mode overlay (2400); both kept. The new
check:frontend-dead passes with Story Mode's scripts in it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants